﻿using System;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Runtime.Serialization;
using System.Windows;

namespace DeepEarth.Core
{
	[TypeConverter(typeof(LocationTypeConverter))]
#if !WINDOWS_PHONE
	[DataContract]
#endif
	public class Location : INotifyPropertyChanged
	{
		const double EarthRadius = 6378137;

		public static string DDMMSSFromDecimal(double decimaldegrees)
		{
            return DDMMSSFromDecimal(decimaldegrees, true);
		}

        public static string DDMMSSFromDecimal(double decimaldegrees, bool unicode)
        {
            double d = Math.Abs(decimaldegrees);
            d += 1.3888888888888888888888888888889e-4;  // add ½ second for rounding  
            var deg = Math.Floor(d);
            var min = Math.Floor((d - deg) * 60);
            var sec = Math.Floor((d - deg - min / 60) * 3600);

            // add leading zeros if required  

            if (unicode)
                return deg.ToString("000") + '\u00B0' + min.ToString("00") + '\u2032' + sec.ToString("00") + '\u2033';
            else
                return deg.ToString("000") + '\u00B0' + min.ToString("00") + "'" + sec.ToString("00") + "\"";
        }


		public static double? DecimalFromDDMMSS(string DDMMSS)
		{
			try
			{
				//detect NSEW and set correctly
				var zone = 1;
				if (DDMMSS.Contains("W") || DDMMSS.Contains("S"))
				{
					zone = -1;
				}
				//strip NSEW
				DDMMSS = DDMMSS.Trim(' ', 'N', 'S', 'E', 'W');
				//split into parts:
				var split1 = DDMMSS.IndexOf('\u00B0');
				var split2 = DDMMSS.IndexOf('\u2032');
				var split3 = DDMMSS.IndexOf('\u2033');
				var deg = double.Parse(DDMMSS.Substring(0, split1));
				var min = double.Parse(DDMMSS.Substring(split1 + 1, split2 - (split1 + 1)));
				var sec = double.Parse(DDMMSS.Substring(split2 + 1, split3 - (split2 + 1)));

				return zone * (deg + (min / 60) + (sec / 3600));
			}
			catch (Exception)
			{
				return null;
			}
		}

		public static double SphericalMercatorFromDecimalLon(double longitude)
		{
			return EarthRadius * Math.PI / 180 * longitude;
		}

		public static double SphericalMercatorFromDecimalLat(double latitude)
		{
			return EarthRadius * Math.Log(Math.Tan(Math.PI / 180 * (45 + latitude / 2.0)));
		}

		public Location() { }

		public Location(double latitude, double longitude)
		{
			Latitude = latitude;
			Longitude = longitude;
		}

		#region Latitude
#if !WINDOWS_PHONE
		[DataMember(EmitDefaultValue=false)]
#endif
		public double Latitude
		{
			get { return mLatitude; }
			set
			{
				if (value == mLatitude)
					return;
				mLatitude = value;
				NotifyPropertyChanged(() => Latitude);
			}
		}
		double mLatitude;

		#endregion

		#region Longitude
		#if !WINDOWS_PHONE
		[DataMember(EmitDefaultValue = false)]
		#endif
		public double Longitude
		{
			get { return mLongitude; }
			set
			{
				if (value == mLongitude)
					return;
				mLongitude = value;
				NotifyPropertyChanged(() => Longitude);
			}
		}
		double mLongitude;

		#endregion

		#region Altitude
		#if !WINDOWS_PHONE
		[DataMember(EmitDefaultValue = false)]
		#endif
		public double Altitude
		{
			get { return mAltitude; }
			set
			{
				if (value == mAltitude)
					return;
				mAltitude = value;
				NotifyPropertyChanged(() => Altitude);
			}
		}
		double mAltitude;

		#endregion

		void NotifyPropertyChanged<T>(Expression<Func<T>> expression)
		{
			if (PropertyChanged != null)
			{
				string propertyName = Utilities.BindingUtilities.PropertyName(expression);
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}

		public event PropertyChangedEventHandler PropertyChanged;

		public Location Clone()
		{
			return new Location
			{
				Altitude = this.Altitude,
				Longitude = this.Longitude,
				Latitude = this.Latitude
			};
		}
		
		// Find mid point between two locations        
		public static Location GetMidpointLocation(Location loc1, Location loc2)
		{
			// As the earth is round, and the map wraps horizontally, there are
			// two ways to draw a line between two points.
			// The bing maps control will draw the shortest line between two points.
			// Calculate the midpoint accordingly
			Location left, right;
			if (loc1.Longitude > loc2.Longitude)
			{
				right = loc1;
				left = loc2;
			}
			else
			{
				right = loc2;
				left = loc1;
			}

			double distanceEast = 180 - right.Longitude + 180 + left.Longitude;
			double distanceWest = right.Longitude - left.Longitude;

			double midLongitude;

			if (distanceEast < distanceWest)
			{
				midLongitude = right.Longitude + (distanceEast / 2.0);
			}
			else
			{
				midLongitude = (loc1.Longitude + loc2.Longitude) / 2.0;
			}

			return new Location((loc1.Latitude + loc2.Latitude) / 2, midLongitude);
		}

		// Find projected mid point between two locations. i.e. if two points are drawn on a map
		// and a straight line drawn between them, this gets the midpoint of that line.
		// http://www.movable-type.co.uk/scripts/latlong.html
		// TODO: This is broken. Problem is my code not above reference.
		//public static Location GetGreatCircleMidpointLocation(Location loc1, Location loc2)
		//{
		//    var deltaLatInRadians = DegToRad(loc2.Latitude - loc1.Latitude);
		//    var deltaLonInRadians = DegToRad(loc2.Longitude - loc1.Longitude); 

		//    var Bx = Math.Cos(loc2.Latitude) * Math.Cos(deltaLonInRadians);
		//    var By = Math.Cos(loc2.Latitude) * Math.Sin(deltaLonInRadians);
		//    var lat3 = Math.Atan2(Math.Sin(loc1.Latitude) + Math.Sin(loc2.Latitude),
		//                          Math.Sqrt((Math.Cos(loc1.Latitude) + Bx) * (Math.Cos(loc1.Latitude) + Bx) + By * By));
		//    var lon3 = loc1.Longitude + Math.Atan2(By, Math.Cos(loc1.Latitude) + Bx);

		//    return new Location
		//    {
		//        Longitude = lon3,
		//        Latitude = lat3
		//    };
		//}

		private static double DegToRad(double deg)
		{
			return deg * Math.PI / 180.0;
		}

		public static Point GetMinDelta(Location loc1, Location loc2)
		{
			double left, right;
			if (loc1.Longitude > loc2.Longitude)
			{
				right = loc1.Longitude;
				left = loc2.Longitude;
			}
			else
			{
				right = loc2.Longitude;
				left = loc1.Longitude;
			}

			double distanceEast = 180 - right + 180 + left;
			double distanceWest = right - left;

			double lonDelta =  Math.Min(distanceEast, distanceWest);

			double north = Math.Max(loc1.Latitude, loc2.Latitude);
			double south = Math.Min(loc1.Latitude, loc2.Latitude);

			double latDelta = north - south;

			return new Point(lonDelta, latDelta);            
		}

		public static double GetAngularDistance(Location loc1, Location loc2)
		{
			Point deltas = Location.GetMinDelta(loc1, loc2);

			return Math.Sqrt(Math.Pow(deltas.X, 2) + Math.Pow(deltas.Y, 2));
		}

		public bool Within(Location location, int accuracy)
		{
			return
				Math.Round(this.Latitude, accuracy) == Math.Round(location.Latitude, accuracy) &&
				Math.Round(this.Longitude, accuracy) == Math.Round(location.Longitude, accuracy) &&
				Math.Round(this.Altitude, accuracy) == Math.Round(location.Altitude, accuracy);
		}

        public bool Equivalent(Location location)
        {
            return
                this.Latitude == location.Latitude &&
                (this.Longitude == location.Longitude ||
                    Math.Abs(this.Longitude) % 180 == 0 && Math.Abs(location.Longitude) % 180 == 0);
        }

		#region Object Overrides
		public override bool Equals(object obj)
		{
			if (obj is Location)
				return ((Location)obj) == this;
			return false;
		}

		public override int GetHashCode()
		{
			return Latitude.GetHashCode() ^ Longitude.GetHashCode() ^ Altitude.GetHashCode();
		}

		public static bool operator==(Location a, Location b)
		{
			if (a == (object)null || b == (object)null) return a == (object)b;

			return a.Longitude == b.Longitude 
					&& a.Latitude == b.Latitude
					&& a.Altitude == b.Altitude;
		}

		public static bool operator !=(Location a, Location b)
		{
			if (a == (object)null || b == (object)null) return a != (object)b;

			return a.Longitude != b.Longitude
					|| a.Latitude != b.Latitude
					|| a.Altitude != b.Altitude;
		}

		#endregion

		public string ToString(string format)
		{
			if (string.IsNullOrWhiteSpace(format))
				return base.ToString();

			string lowerFormat = format.ToLower();

			CoordinateSystem locationFormat;

			bool success = false;

#if !WINDOWS_PHONE            

            success = Enum.TryParse<CoordinateSystem>(format, true, out locationFormat);
#else
			try
			{
				locationFormat = (LocationFormat)Enum.Parse(typeof(LocationFormat), format, true);
				success = true;
			}
			catch
			{
				// Just setting it to some value here to satisfy the compiler.
				locationFormat = LocationFormat.LatLon;
			}
#endif

			if (success)
			{
				switch (locationFormat)
				{
                    case CoordinateSystem.DDMMSS: //DDMMSS
						return DDMMSSFromDecimal(this.Latitude) +
							   ((this.Latitude > 0) ? "N " : "S ") + " " +
							   DDMMSSFromDecimal(this.Longitude) +
							   ((this.Longitude > 0) ? "E " : "W ");

                    case CoordinateSystem.UTM: //UTM
						return base.ToString();

					// DON'T want a dependency on ProjNet at this point

					//var ctfac = new CoordinateTransformationFactory();
					//var wgs84geo = GeographicCoordinateSystem.WGS84;
					//var zone = (int)Math.Ceiling((this.Longitude + 180) / 6);
					//var utm = ProjectedCoordinateSystem.WGS84_UTM(zone, this.Latitude > 0);
					//var trans = ctfac.CreateFromCoordinateSystems(wgs84geo, utm);
					//Point pUtm = trans.MathTransform.Transform(new Point(this.Longitude, this.Latitude));
					//return string.Format("E{0} ", Math.Round(pUtm.X, 0)) + " " +
					//       string.Format("N{0}", Math.Round(pUtm.Y, 0)) +
					//       string.Format(" ({0}{1})", zone, this.Latitude > 0 ? 'N' : 'S');


					default: //Decimal Degrees (0)
						return string.Format("{0:N4},", this.Latitude) + " " +
							   string.Format("{0:N4}", this.Longitude);
				}
			}

			return base.ToString();
		}

	}
}